package com.avcompris.util.junit;

import static com.avcompris.util.ExceptionUtils.nonNullArgument;
import static com.avcompris.util.PropertiesUtils.filterEnvironmentVariables;
import static com.avcompris.util.junit.AvcMatchers.fileExists;
import static org.apache.commons.io.FileUtils.openInputStream;
import static org.apache.commons.lang3.CharEncoding.UTF_8;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import junit.framework.AssertionFailedError;

import org.junit.Assert;

import com.avcompris.common.annotation.Nullable;
import com.avcompris.util.AbstractUtils;

/**
 * assertions on files. Use this class instead of junit-addons 1.4's FileAssert,
 * since sometimes it bugs.
 * 
 * @author David Andriana Copyright Avantage Compris SARL 2008-2009 ©
 */
public abstract class AvcFileAssert extends AbstractUtils {

	/**
	 * assert two text files are equal.
	 * 
	 * @param refFile the reference file
	 * @param file the actual file
	 */
	public static void assertTextEquals(
			final File refFile,
			final File file) throws IOException {

		assertContentComparison(refFile, file, new ContentComparisonCallback() {

			@Override
			public void assertContentEquals(
					final BufferedReader refBr,
					final BufferedReader br) throws IOException {

				for (int lineCount = 1;; ++lineCount) {

					final String refLine = refBr.readLine();
					final String line = br.readLine();

					if (refLine == null && line == null) {
						break;
					}

					if (refLine == null) {
						fail(lineCount + ", expected: <EOF>, but was: <" + line
								+ ">");
					}

					if (line == null) {
						fail(lineCount + ", expected: <" + refLine
								+ ">, but was: <EOF>");
					}

					if (refLine.equals(line)) {
						continue;
					}

					assertTextLineEquals(lineCount, refLine, line);
				}
			}
		});
	}

	/**
	 * assert two text lines are equal.
	 * 
	 * @param lineCount the current line number
	 * @param refLine the reference text line
	 * @param line the test text line
	 */
	private static void assertTextLineEquals(
			final int lineCount,
			final String refLine,
			final String line) {

		nonNullArgument(refLine, "refLine");
		nonNullArgument(line, "line");

		if (!refLine.equals(line)) {

			if (refLine.trim().equals(line.trim())) {

				if (refLine.startsWith(line)) {
					Assert.assertEquals(lineCount
							+ ". Expected trailing spaces not found: ["
							+ renderSpaces(refLine.substring(line.length()))
							+ "],", refLine, line);
				}

				if (line.startsWith(refLine)) {
					Assert.assertEquals(lineCount
							+ ". Found unexpected trailing spaces: ["
							+ renderSpaces(line.substring(refLine.length()))
							+ "],", refLine, line);
				}
			}

			Assert.assertEquals(lineCount + ",", refLine, line);
		}
	}

	/**
	 * assert two text files are equal.
	 * 
	 * @param refFile the reference file
	 * @param file the actual file
	 */
	public static void assertRegexpEquals(
			final File refFile,
			final File file) throws IOException {

		assertContentComparison(refFile, file, new ContentComparisonCallback() {

			@Override
			public void assertContentEquals(
					final BufferedReader refBr,
					final BufferedReader br) throws IOException {

				for (int lineCount = 1;; ++lineCount) {

					final String refLine = refBr.readLine();
					final String line = br.readLine();

					if (refLine == null && line == null) {
						break;
					}

					if (refLine == null) {
						fail(lineCount + ", expected: <EOF>, but was: <" + line
								+ ">");
					}

					if (line == null) {
						fail(lineCount + ", expected: <" + refLine
								+ ">, but was: <EOF>");
					}

					if (refLine.equals(line)) {
						continue;
					}

					if (refLine.contains("\\")) {

						// even-indexed tokens are: Litteral Strings
						// odd-indexed tokens are: Regexps

						final List<String> refTokens = new ArrayList<String>();

						if (refLine.startsWith("\\")) {
							refTokens.add("");
						}

						for (final StringTokenizer tokenizer = new StringTokenizer(
								refLine, "\\", false); tokenizer
								.hasMoreTokens();) {

							final String refToken = tokenizer.nextToken();

							refTokens.add(refToken);
						}

						if (refLine.endsWith("\\")) {
							refTokens.add("");
						}

						final int length = refTokens.size();

						if (length % 2 == 0) { // That is, != 1
							throw new RuntimeException(
									"Illegal Regexps in refLine: [" + refLine
											+ "]");
						}

						int index = 0;

						for (int i = 0; i < length; i += 2) {

							final String refToken = refTokens.get(i);

							final int refTokenLength = refToken.length();

							final String token;

							if (i == length - 1) {

								token = line.substring(index);

							} else {

								token = line.substring(index, index
										+ refTokenLength);
							}

							Assert.assertEquals(lineCount + ", " + refLine,
									refToken, token);

							index += refTokenLength;

							if (i == length - 1) {
								continue;
							}

							final String refRegexp = refTokens.get(i + 1);

							final String nextToken = refTokens.get(i + 2);

							final int nextIndex = "".equals(nextToken) ? line
									.length() : line.indexOf(nextToken, index);

							if (nextIndex == -1) {
								Assert.fail("Cannot find token <" + nextToken
										+ "> in line <" + line
										+ ">, expected Regexp: <" + refLine
										+ ">");
							}

							final String match = line.substring(index,
									nextIndex);

							final Pattern pattern = Pattern.compile(refRegexp);

							final Matcher matcher = pattern.matcher(match);

							if (!matcher.matches()) {
								Assert.fail("<" + match
										+ "> doesn't match Regexp <"
										+ refRegexp + "> in line <" + line
										+ ">, expected Regexp: <" + refLine
										+ ">");
							}

							index = nextIndex;
						}

					} else {

						assertTextLineEquals(lineCount, refLine, line);
					}
				}
			}
		});
	}

	/**
	 * assert two text files are equal.
	 * 
	 * @param refFile the reference file
	 * @param file the actual file
	 * @param properties the filtering properties
	 */
	public static void assertProcessedEquals(
			final File refFile,
			final File file,
			@Nullable final Properties properties) throws IOException {

		assertContentComparison(refFile, file, new ContentComparisonCallback() {

			@Override
			public void assertContentEquals(
					final BufferedReader refBr,
					final BufferedReader br) throws IOException {

				for (int lineCount = 1;; ++lineCount) {

					final String refLine = refBr.readLine();
					final String line = br.readLine();

					if (refLine == null && line == null) {
						break;
					}

					if (refLine == null) {
						fail(lineCount + ", expected: <EOF>, but was: <" + line
								+ ">");
					}

					if (line == null) {
						fail(lineCount + ", expected: <" + refLine
								+ ">, but was: <EOF>");
					}

					if (refLine.equals(line)) {
						continue;
					}

					if (refLine.contains("${")) {

						final String processedRefLine = filterEnvironmentVariables(
								refLine, properties);

						assertTextLineEquals(lineCount, processedRefLine, line);

					} else {

						assertTextLineEquals(lineCount, refLine, line);
					}
				}
			}
		});
	}

	/**
	 * callback to use when parsing two files for comparison.
	 * 
	 * @David Andriana
	 */
	private interface ContentComparisonCallback {

		/**
		 * this method will be called within a I/O comparison method template.
		 * 
		 * @param refBr the reader to the reference content
		 * @param br the reader to the actual content
		 */
		void assertContentEquals(
				BufferedReader refBr,
				BufferedReader br) throws IOException;
	}

	/**
	 * I/O comparison method template.
	 * 
	 * @param refFile the reference file
	 * @param file the actual file
	 * @param callback the callback to use
	 */
	private static void assertContentComparison(
			final File refFile,
			final File file,
			final ContentComparisonCallback callback) throws IOException {

		nonNullArgument(refFile, "refFile");
		nonNullArgument(file, "file");
		nonNullArgument(callback, "callback");

		final InputStream refIs = openInputStream(refFile);
		try {
			final InputStream is = openInputStream(file);
			try {
				final Reader refReader = new InputStreamReader(refIs, UTF_8);
				try {
					final BufferedReader refBr = new BufferedReader(refReader);
					try {
						final Reader reader = new InputStreamReader(is, UTF_8);
						try {
							final BufferedReader br = new BufferedReader(reader);
							try {

								callback.assertContentEquals(refBr, br);

							} finally {
								br.close();
							}
						} finally {
							reader.close();
						}
					} finally {
						refBr.close();
					}
				} finally {
					refReader.close();
				}
			} finally {
				is.close();
			}
		} finally {
			refIs.close();
		}
	}

	/**
	 * assert two code files are equal.
	 * 
	 * @param refFile the reference file
	 * @param file the actual file
	 */
	public static void assertCodeEquals(
			final File refFile,
			final File file) throws IOException {

		assertContentComparison(refFile, file, new ContentComparisonCallback() {

			@Override
			public void assertContentEquals(
					final BufferedReader refBr,
					final BufferedReader br) throws IOException {

				for (int lineCount = 1;; ++lineCount) {

					final String refLine = refBr.readLine();
					final String line = br.readLine();

					if (refLine == null && line == null) {
						break;
					}

					if (refLine == null) {
						fail(lineCount + ", expected: <EOF>, but was: <" + line
								+ ">");
					}

					if (line == null) {
						fail(lineCount + ", expected: <" + refLine
								+ ">, but was: <EOF>");
					}

					if (refLine.equals(line)) {
						continue;
					}

					if (refLine.trim().equals(line.trim())
							&& (refLine.startsWith(line) || line
									.startsWith(refLine))) {
						continue;
					}

					Assert.assertEquals(lineCount + ",", refLine, line);
				}
			}
		});
	}

	/**
	 * render spaces in a {@link String}.
	 * 
	 * @param s the original {@link String}
	 * @return the {@link String} ready for rendition
	 */
	public static String renderSpaces(
			final String s) {

		nonNullArgument(s, "s");

		final StringBuilder sb = new StringBuilder();

		for (final char c : s.toCharArray()) {

			if (Character.isWhitespace(c)) {

				switch (c) {
				case ' ':
					sb.append("\\s");
					break;
				case '\t':
					sb.append("\\t");
					break;
				case '\r':
					sb.append("\\r");
					break;
				case '\n':
					sb.append("\\n");
					break;
				default:
					sb.append('(').append((int) c).append(')');
					break;
				}
			} else {

				sb.append(c);
			}
		}

		return sb.toString();
	}

	/**
	 * assert a byte array is equal to a Ref File.
	 * <p>
	 * This code has been copied/pasted from
	 * <tt>junitx.framework.FileAssert</tt>, then rearranged a little.
	 * 
	 * @param message the message to display in case of failure
	 * @param expected the reference file
	 * @param actual the actual read byte
	 */
	public static void assertEquals(
			@Nullable final String message,
			final File expected,
			final byte[] actual) throws IOException {

		assertEquals(message, expected, actual, UTF_8);
	}

	/**
	 * assert a byte array is equal to a Ref File.
	 * 
	 * @param expected the reference file
	 * @param actual the actual read byte
	 */
	public static void assertEquals(
			final File expected,
			final byte[] actual) throws IOException {

		assertEquals(null, expected, actual);
	}

	/**
	 * assert a byte array is equal to a Ref File.
	 * <p>
	 * This code has been copied/pasted from
	 * <tt>junitx.framework.FileAssert</tt>, then rearranged a little.
	 * 
	 * @param message the message to display in case of failure
	 * @param expected the reference file
	 * @param actual the actual read byte
	 * @param encoding the encoding to use for the comparison
	 */
	public static void assertEquals(
			@Nullable final String message,
			final File expected,
			final byte[] actual,
			final String encoding) throws IOException {

		nonNullArgument(expected, "expected");
		nonNullArgument(actual, "actual");
		nonNullArgument(encoding, "encoding");

		assertThat(expected, fileExists(expected.getAbsolutePath()));
		assertTrue("Expected file not readable", expected.canRead());

		final InputStream eis = openInputStream(expected);
		try {
			final InputStream ais = new ByteArrayInputStream(actual);
			try {
				final Reader expReader = new InputStreamReader(eis, encoding);
				try {
					final BufferedReader expData = new BufferedReader(expReader);
					try {
						final Reader actReader = new InputStreamReader(ais,
								encoding);
						try {
							final BufferedReader actData = new BufferedReader(
									actReader);
							try {

								assertNotNull(message, expData);
								assertNotNull(message, actData);

								assertEquals(message, expData, actData);

							} finally {
								actData.close();
							}
						} finally {
							actReader.close();
						}
					} finally {
						expData.close();
					}
				} finally {
					expReader.close();
				}
			} finally {
				ais.close();
			}
		} finally {
			eis.close();
		}
	}

	/**
	 * <b>Testing only</b> Asserts that two readers are equal. Throws an
	 * {@link AssertionFailedError} if they are not.
	 * <p>
	 * This code has been copied/pasted from
	 * <tt>junitx.framework.FileAssert</tt>, then rearranged a little.
	 * 
	 * @param message the message to display in case of failure
	 * @param expected a reder to the reference content
	 * @param actual a reader to the actual content
	 */
	private static void assertEquals(
			@Nullable final String message,
			final Reader expected,
			final Reader actual) throws IOException {

		nonNullArgument(expected, "expected");
		nonNullArgument(actual, "actual");

		final LineNumberReader expReader = new LineNumberReader(expected);
		final LineNumberReader actReader = new LineNumberReader(actual);

		final StringBuilder formatted = new StringBuilder();

		if (message != null) {
			formatted.append(message).append(' ');
		}

		while (true) {
			if (!expReader.ready() && !actReader.ready()) {
				return;
			}

			final String expLine = expReader.readLine();
			final String actLine = actReader.readLine();

			if (expLine == null && actLine == null) {
				return;
			}

			final int line = expReader.getLineNumber() + 1;

			if (expReader.ready()) {
				if (actReader.ready()) {
					Assert.assertEquals(formatted + "Line [" + line + "]",
							expLine, actLine);
				} else {
					fail(formatted + "Line [" + line + "] expected <" + expLine
							+ "> but was <EOF>");
				}
			} else {
				if (actReader.ready()) {
					fail(formatted + "Line [" + line
							+ "] expected <EOF> but was <" + actLine + ">");
				} else {
					Assert.assertEquals(formatted + "Line [" + line + "]",
							expLine, actLine);
				}
			}
		}
	}
}
